import { Store, StoreItem, createStore } from "./store";
import { AutoInject } from "./inject";
import { provideMethodName } from "./utils";

/**
 *  typeof any item of the store in Event
 */
interface EventStoreItem {
	target: any;
	args?: any[];
	propertyKey: string;
	tag: string | null;
}

/**
 *  Emit events in EventController Class
 */
export class StoreEmitter {
	constructor(private storeItem: StoreItem<EventStoreItem>[]) {}

	emit(...args: any[]) {
		this.storeItem.forEach(item => {
			if (item.value["target"] && item.value["propertyKey"]) {
				const target = item.value["target"];
				const propertyKey = item.value["propertyKey"];
				if (target[propertyKey] !== undefined) {
					target[propertyKey].call(target, ...args);
				}
			}
		});
	}
}

/**
 *  Store and manage all event
 */
export class EventController {
	constructor(private store: Store) {}

	emit(...args: any[]) {
		return this.emitHook().emit(...args);
	}

	getStore() {
		return this.store;
	}

	includeTags(tag: string | string[]) {
		const checkTags = (tagName: string | null) => {
			if (typeof tag === "string") {
				return typeof tagName === "string" && tagName.includes(tag);
			} else {
				return typeof tagName === "string" && tag.includes(tagName);
			}

			return false;
		};

		const items = this.store.items.filter(item => {
			if (item.value.tag !== undefined) {
				return checkTags(item.value.tag);
			}
			return false;
		});

		return this.emitHook(items);
	}

	selectTags(tag: string | null | ((tagName: string | null) => boolean)) {
		const checkTags = (tagName: string | null) => {
			if (typeof tag === "string" || typeof tag === "object") {
				return tagName === tag;
			} else {
				return tag(tagName);
			}
		};
		const items = this.store.items.filter(item => {
			if (item.value.tag !== undefined) {
				return checkTags(item.value.tag);
			}
			return false;
		});

		return this.emitHook(items);
	}

	private emitHook(items?: StoreItem<any>[]) {
		return new StoreEmitter(
			items
				? items
				: this.store.items.filter(item => item.value.tag !== undefined)
		);
	}
}

/**
 * Provide a event injector for event dealing
 */
export class EventInjector {
	public store: Store;
	public controller: EventController;

	constructor() {
		this.store = createStore();
		this.controller = new EventController(this.store);
	}

	Subject = (...args: any[]) => {
		return (target: any) => {
			const newTarget = AutoInject(target, ...args);
			for (let i in this.controller.getStore().items) {
				const item = this.controller.getStore().items[i];
				if (
					item.value.target !== undefined &&
					item.value.target.constructor !== undefined &&
					item.value.propertyKey !== undefined &&
					item.value.tag !== undefined
				) {
					if (String(item.value.target.constructor) === String(target)) {
						item.value.target = newTarget;
					}
				}
			}
		};
	};

	Provide = () => {
		return (
			target: any,
			propertyKey: string,
			descriptor: PropertyDescriptor
		) => {
			this.store.add({
				name: provideMethodName(target, propertyKey, descriptor),
				value: {
					target,
					propertyKey,
					tag: null
				} as EventStoreItem
			});
		};
	};

	Tag = (tag?: string) => {
		return (
			target: any,
			propertyKey: string,
			descriptor: PropertyDescriptor
		) => {
			const storeItemName = provideMethodName(target, propertyKey, descriptor);
			const noHashGetItem = this.controller
				.getStore()
				.get(storeItemName, false);
			if (noHashGetItem.tag !== undefined) {
				noHashGetItem.tag = tag || null;
			}
		};
	};
}

/**
 * create a Event Module
 */
export function createEvent() {
	return new EventInjector();
}
